/* File: Simulation.java
 * Purpose: Represents a group of organisms in a petri dish
 * Author: Team Karp
 * Date: 12/13/07
 * Revisions: 12/13/07 - Initial Version - Adam Patterson
 *              1/10/08 - Draw method outlined - Adam Patterson
 *              3/16/08 - Added two utility functions for getting organisms that are close to you.
 * Additional Requirements: 
 */
package protovitae.data;

import com.sun.opengl.util.BufferUtil;
import com.sun.org.apache.xml.internal.serialize.XMLSerializer;
import java.awt.Color;
import java.io.*;
import java.nio.IntBuffer;
import java.util.*;
import javax.media.opengl.GL;
import javax.media.opengl.glu.GLU;
import javax.swing.JOptionPane;
import protovitae.data.Organism.ATTRIBUTE_TYPE;
import protovitae.engine.*;
import protovitae.ui.EnvironmentView;
import org.w3c.dom.*;
import javax.xml.parsers.*;
import org.xml.sax.SAXException;
import protovitae.ui.FileXMLFilter;

/**
 * @version 1.0
 * @created 06-Dec-2007 16:19:10
 */
public class Simulation extends Thread {

    private final int BUFSIZE = 512;
    /** The number of cycles to be run. */
    private long cycles;
    private long cycleLength; // in ms.
    /** Health lost during each cycle **/
    private int healthmod;
    /** The List of Organisms in the simulation */
    private ArrayList<DrawableOrganism> organisms = new ArrayList<DrawableOrganism>();
    private TraitTree masterlist; //The master list of traits in this simulation
    private EnvironmentView canvas = null;
    /** The radius of the petri dish */
    private int radiusOfMovement = 100;
    private DrawableOrganism selectedOrganism = null;
    private ArrayList<Explosion> deaths = new ArrayList<Explosion>();
    private DrawableDish dish = new DrawableDish(radiusOfMovement, 3);
    /** Thread control **/
    private int mode = 0; //Current thread state. See below for why we need to do this. Avoided using an enum since only 4 states.
    //States: 0 = Stopped, 1 = Running, 2 = Paused, 3 = Step.
    //*** Begin simulation statistics ***/
    private int numAdded,  numDeleted,  numDeaths; //Some basic statistics
    private long numCycles; //Number of cycles we ran for.
    //*** End of Simulation statistics **/
    public Simulation(EnvironmentView viewport) {
        cycles = 10000; //Default value
        cycleLength = 250;
        healthmod = 0;
        this.canvas = viewport;

        //Initialize stats
        numAdded = 0;
        numDeleted = 0;
        numDeaths = 0;
        numCycles = cycles;
        masterlist = new TraitTree(organisms); //Initialize the master list of stock traits and traits inside organisms.
        loadStockTraits();
    }

    /**
     * Add the organism to the simulation
     * @param myorg
     */
    public synchronized boolean addOrganism(ConcreteOrganism myorg) {
        int tries = 0;
        if (locationAvailable(myorg.getPosition(), myorg.getSize(), myorg)) {
            organisms.add(new DrawableOrganism(myorg,this));
        } else {
            //randomize location?
            Position possible = new Position(0, 0, 0);
            while (!locationAvailable(possible, myorg.getSize(), myorg) && tries < 500) {
                tries++;
                double x = -this.getRadiusOfMovement() + Math.random() * 2 * this.getRadiusOfMovement();
                double y = -this.getRadiusOfMovement() + Math.random() * 2 * this.getRadiusOfMovement();
                possible = new Position(x, y, 0);
            }
            if (tries == 500) {
                return false; //Fail
            }

            myorg.setPosition(possible);
            organisms.add(new DrawableOrganism(myorg,this));
        }
        myorg.setParentSimulation(this);
        numAdded++;
        return true; //Sucess
    }

    /**
     * Adds an organism around a specific location. 
     * @param myorg
     * @param position
     */
    void addOrganism(ConcreteOrganism myorg, Position position) {
        int tries = 0;
        Position possible = new Position(position.getX(), position.getY(), position.getZ());
        Random random = new Random();
        int count = 0;
        boolean yAxisTendancy = random.nextBoolean();
        boolean xAxisTendancy = random.nextBoolean();
        while (!locationAvailable(possible, myorg.getSize(), myorg) && tries < 500) {
            tries++;
            ++count;
            possible.setX(possible.getX() + (random.nextBoolean() ? +1 : xAxisTendancy ? +1 : -1));
            possible.setY(possible.getY() + (random.nextBoolean() ? +1 : yAxisTendancy ? +1 : -1));
            if (count == 1000) { // Recalibrate the possible positions if we go crazy.
                count = 0;
                possible = new Position(position.getX(), position.getY(), position.getZ());
                yAxisTendancy = random.nextBoolean();
                xAxisTendancy = random.nextBoolean();
            }
        }

        if (tries == 500) {
            return;
        }

        myorg.setPosition(possible);
        organisms.add(new DrawableOrganism(myorg,this));
        myorg.setParentSimulation(this);
        numAdded++;
    }

    public synchronized void addOrganism(DrawableOrganism organism) {
        organisms.add(organism);
        numAdded++;
    }

    public ConcreteOrganism getConcerteOrganism(int index) {
        return (ConcreteOrganism) organisms.get(index);
    }

    public DrawableOrganism getDrawableOrganism(int index) {
        return organisms.get(index);
    }

    /**
     * 
     * @param simsel
     */
    public void loadSim(File simsel) {

    }

    /**
     * Start the Thread
     */
    @Override
    public void run() {
        System.out.println("Thread started.");
        mode = 1;
        while (cycles > 0 && mode > 0) {
            switch (mode) {
                case 0:
                default:
                    break;
                case 1: //Run
                    runCycle();
                    break;
                case 2: //Paused
                    try {
                        Thread.sleep(20);
                    } catch (Exception e) {
                    }
                    break;
                case 3: //Step
                    runCycle();
                    mode = 2;
                    break;
            }

        }
        JOptionPane.showMessageDialog(canvas, "This simulation is over."); //To do: You can't run again!! We need to take the user to the postmortem.
        canvas.getMainWindow().endSimulation(); //Tell the main window we're done.
    }

    /** Pause the simulation, or step one cycle if it's paused **/
    public void pauseSim() {
        if (mode == 2) {
            mode = 3;
        } //Paused
        else {
            mode = 2;
        }
    }

    /** Kill the simulation **/
    public void endSim() {
        cycles = 0;
        mode = 0;
    }

    /** Resume playback **/
    public void resumeSim() {
        mode = 1;
    }

    /**
     * Delete an organism from the simulation
     * @param deleting The organism we're deleting
     */
    public synchronized void deleteOrganism(ConcreteOrganism deleting) {
        organisms.remove(deleting);
        numDeleted++;
    }

    /**
     * Run the simulation for one cycle
     * 
     * Removed synchronized: synchronized does not allow control to be released to the GUI
     * when the simulation is running, i.e., GUI will freeze while simulation started.
     */
    private void runCycle() {
        try {
            long beginTimeMs = System.currentTimeMillis();
            cycles--;
            //perform actions
            for (int i = 0; i < organisms.size(); ++i) {
                organisms.get(i).act(this);
                if (organisms.get(i).checkLife()) {
                    organisms.get(i).setAttributeValue(ATTRIBUTE_TYPE.HEALTH, organisms.get(i).getAttribute(ATTRIBUTE_TYPE.HEALTH).getCurrent() - healthmod);
                }
            }

            //clean up dead and empty things
            ArrayList<ConcreteOrganism> toRemove = new ArrayList<ConcreteOrganism>();
            for (int i = 0; i < organisms.size(); ++i) {
                if (!organisms.get(i).exists()) {
                    toRemove.add(organisms.get(i));
                    deaths.add(new Explosion(organisms.get(i).getColor(), organisms.get(i).getPosition()));
                    numDeaths++;
                }
            }
            organisms.removeAll(toRemove);

            //remove old explosive deaths
            ArrayList<Explosion> exToRemove = new ArrayList<Explosion>();
            for (int i = 0; i < deaths.size(); ++i) {
                if (!deaths.get(i).continueBlowingUp()) {
                    exToRemove.add(deaths.get(i));
                }
            }
            deaths.removeAll(exToRemove);

            //TODO: Environmental affects
            //Randomize organisms to make life more fair
            Collections.shuffle(organisms);

            long elapsedTimeMs = System.currentTimeMillis() - beginTimeMs;
            long timeToWait = elapsedTimeMs < cycleLength ? cycleLength - elapsedTimeMs : 0;

            Thread.sleep(timeToWait);
        //System.out.println("Cycle " + cycles + " complete.");
        } catch (InterruptedException ex) {
            JOptionPane.showMessageDialog(canvas, "Thread interrupted.");
        }
    }

    /**
     * 
     * @param savefile
     */
    public void saveSim(File savefile) {
        try {
            //Create initial XML Document and root element
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder loader = factory.newDocumentBuilder();
            Document saveDocument = loader.newDocument();

            Element root = saveDocument.createElement("simulation");
            saveDocument.appendChild(root);

            //append all simulation properties
            Element emCycles = saveDocument.createElement("cycles");
            emCycles.appendChild(saveDocument.createTextNode("" + this.cycles));
            root.appendChild(emCycles);

            Element emCycleLength = saveDocument.createElement("cycleLength");
            emCycleLength.appendChild(saveDocument.createTextNode("" + this.cycleLength));
            root.appendChild(emCycleLength);

            Element emHealthMod = saveDocument.createElement("healthMod");
            emHealthMod.appendChild(saveDocument.createTextNode("" + this.healthmod));
            root.appendChild(emHealthMod);

            Element emRadiusOfMovement = saveDocument.createElement("radiusOfMovement");
            emRadiusOfMovement.appendChild(saveDocument.createTextNode("" + this.radiusOfMovement));
            root.appendChild(emRadiusOfMovement);

            Element emNumCycles = saveDocument.createElement("numCycles");
            emNumCycles.appendChild(saveDocument.createTextNode("" + this.numCycles));
            root.appendChild(emNumCycles);

            Element emNumDeaths = saveDocument.createElement("numDeaths");
            emNumDeaths.appendChild(saveDocument.createTextNode("" + this.numDeaths));
            root.appendChild(emNumDeaths);

            Element emNumDeleted = saveDocument.createElement("numDeleted");
            emNumDeleted.appendChild(saveDocument.createTextNode("" + this.numDeleted));
            root.appendChild(emNumDeleted);

            Element emNumAdded = saveDocument.createElement("numAdded");
            emNumAdded.appendChild(saveDocument.createTextNode("" + this.numAdded));
            root.appendChild(emNumAdded);

            //append organisms
            Element emOrganisms = saveDocument.createElement("organisms");
            for (DrawableOrganism dorg : this.organisms) {
                dorg.save(emOrganisms, saveDocument);
            }
            root.appendChild(emOrganisms);

            //TODO: Save master list of traits?

            //save to file
            XMLSerializer serializer = new XMLSerializer();
            FileWriter fw = new FileWriter(savefile);
            serializer.setOutputCharStream(fw);
            serializer.serialize(saveDocument);

            serializer.endDocument();
            fw.close();
        } catch (IOException ioe) {
            ioe.printStackTrace();
        } catch (ParserConfigurationException pce) {
            pce.printStackTrace();
        } catch (SAXException se) {
            se.printStackTrace();
        }
    }

    public static Simulation loadSim(EnvironmentView viewport, File loadFile) {
        Simulation newSim = new Simulation(viewport);

        try {
            //Create initial XML Document and root element
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder loader = factory.newDocumentBuilder();
            Document loadDocument = loader.parse(loadFile);

            NodeList listOfNodes = loadDocument.getChildNodes();
            for (int i = 0; i < listOfNodes.getLength(); ++i) {
                Node simulationNodes = listOfNodes.item(i);

                NodeList subListOfNodes = simulationNodes.getChildNodes();
                for (int j = 0; j < subListOfNodes.getLength(); ++j) {
                    Node memberNode = subListOfNodes.item(j);

                    if (memberNode.getNodeName().equals("cycles")) {
                        newSim.cycles = Long.parseLong(memberNode.getChildNodes().item(0).getNodeValue());
                    }
                    if (memberNode.getNodeName().equals("cycleLength")) {
                        newSim.cycleLength = Long.parseLong(memberNode.getChildNodes().item(0).getNodeValue());
                    }
                    if (memberNode.getNodeName().equals("healthMod")) {
                        newSim.healthmod = Integer.parseInt(memberNode.getChildNodes().item(0).getNodeValue());
                    }
                    if (memberNode.getNodeName().equals("radiusOfMovement")) {
                        newSim.radiusOfMovement = Integer.parseInt(memberNode.getChildNodes().item(0).getNodeValue());
                    }
                    if (memberNode.getNodeName().equals("numCycles")) {
                        newSim.numCycles = Long.parseLong(memberNode.getChildNodes().item(0).getNodeValue());
                    }
                    if (memberNode.getNodeName().equals("numDeaths")) {
                        newSim.numDeaths = Integer.parseInt(memberNode.getChildNodes().item(0).getNodeValue());
                    }
                    if (memberNode.getNodeName().equals("numDeleted")) {
                        newSim.numDeleted = Integer.parseInt(memberNode.getChildNodes().item(0).getNodeValue());
                    }
                    if (memberNode.getNodeName().equals("numAdded")) {
                        newSim.numAdded = Integer.parseInt(memberNode.getChildNodes().item(0).getNodeValue());
                    }
                    if (memberNode.getNodeName().equals("organisms")) {
                        NodeList organismNodes = memberNode.getChildNodes();
                        for (int k = 0; k < organismNodes.getLength(); ++k) {
                            Node organismNode = organismNodes.item(k);
                            if(organismNode.getNodeName().equals("organism")){
                                ConcreteOrganism tempCO = new ConcreteOrganism(newSim);
                                tempCO.load((Element)organismNode);
                                newSim.organisms.add(new DrawableOrganism(tempCO,newSim));
                            }
                        }
                    }
                }
            }

        } catch (IOException ioe) {
            ioe.printStackTrace();
        } catch (ParserConfigurationException pce) {
            pce.printStackTrace();
        } catch (SAXException saxe){
            saxe.printStackTrace();
        }
        return newSim;
    }

    /**
     * Draw all the organisms to the current OpenGL context
     * @param gl
     */
    public synchronized void draw(GL gl) {

        for (int i = 0; i < organisms.size(); i++) {
            if (organisms.get(i).equals(selectedOrganism)) {
                organisms.get(i).render(gl, GL.GL_RENDER, i + 1, true);
                ((EnvironmentView) this.canvas).getMainWindow().setStats(organisms.get(i));
            } else {
                organisms.get(i).render(gl, GL.GL_RENDER, i + 1, false);
            }
        }
        for (int i = 0; i < deaths.size(); i++) {
            deaths.get(i).render(gl);
        }

        gl.glColor3f(1.0f, 1.0f, 1.0f); //Set dish polygon color to white, so it doesn't have a colored tint.
        dish.render(gl);
        gl.glDisable(GL.GL_TEXTURE_2D);
    }

    /**
     * Determines if an organism can move to a specific area
     * @param targetLocation - the Position an organism wants to move to
     * @param radius - the size of the moving organism
     * @return - true if that area is clear and within an acceptable range
     */
    public boolean locationAvailable(Position targetLocation, double radius, ConcreteOrganism mover) {
        try {
            //test 1: Inside the dish (dish lies on x,y plane)
            double distanceFromCenter = Math.sqrt(
                    Math.pow(targetLocation.getX(), 2) + Math.pow(targetLocation.getY(), 2));
            if (distanceFromCenter + radius > this.getRadiusOfMovement()) {
                //outside the dish, quit
                return false;
            }

            double dx = 0;  //x separation
            double dy = 0;  //y separation
            double d = 0;   //radial distance apart
            for (DrawableOrganism test : organisms) {
                if (!test.equals(mover)) {
                    dx = test.getPosition().getX() - targetLocation.getX();
                    dy = test.getPosition().getY() - targetLocation.getY();
                    d = Math.sqrt(dy * dy + dx * dx);
                    if (d < radius + test.getSize()) {
                        return false;   //organisms would intersect
                    }
                }
            }
            return true;
        } catch (ConcurrentModificationException ex) {
            return false;
        }
    }

    /**
     * Overloaded move check method.  See locationAvailable(Position,double,ConcreteOrganism)
     * @param x - target x location
     * @param y - target y location
     * @param z - target z location
     * @param mover - organism moving
     * @return - true if that area is available
     */
    public boolean locationAvailable(double x, double y, double z, ConcreteOrganism mover) {
        return locationAvailable(new Position(x, y, z), mover.getSize(), mover);
    }

    /**
     * 
     * @param gl
     * @param mouseX
     * @param mouseY
     * @return
     */
    public synchronized ConcreteOrganism processSelections(GL gl, int mouseX, int mouseY) {
        int[] viewport = new int[4];
        gl.glGetIntegerv(GL.GL_VIEWPORT, viewport, 0);
        GLU glu = new GLU();

        IntBuffer selectionBuf = BufferUtil.newIntBuffer(BUFSIZE);
        int hitCount = 0;


        gl.glLoadIdentity();
        glu.gluPickMatrix(mouseX, viewport[3] - mouseY, .4d, .4d, viewport, 0);
        gl.glOrtho(-100.0, 100.0, -100.0, 100.0, -100.0, 100.0);
        gl.glMatrixMode(GL.GL_MODELVIEW);

        gl.glPushMatrix();
        gl.glSelectBuffer(selectionBuf.capacity(), selectionBuf);
        gl.glRenderMode(GL.GL_SELECT);

        gl.glInitNames();
        gl.glPushName(0); //Do we need to pop these? Something might overflow later.

        for (int i = 0; i < organisms.size(); i++) {
            organisms.get(i).render(gl, GL.GL_SELECT, i + 1, false);
        }

        hitCount = gl.glRenderMode(GL.GL_RENDER);
        gl.glPopMatrix();

        return processHits(hitCount, selectionBuf);
    }

    /**
     * 
     * @param hitCount
     * @param selectionBuf
     * @return
     */
    private ConcreteOrganism processHits(int hitCount, IntBuffer selectionBuf) {
        int[] bufferArray = new int[selectionBuf.capacity()];
        selectionBuf.get(bufferArray);
        ConcreteOrganism hit = null;

        if (hitCount > 0) {
            if (bufferArray[3] > 0 && bufferArray[3] <= organisms.size()) {
                int idx = bufferArray[3];
                hit = organisms.get(idx - 1);
                this.selectedOrganism = organisms.get(idx - 1);
            }
        }

        return hit;
    }

    /** Get the organisms in the current simulation **/
    public ArrayList<DrawableOrganism> getOrganisms() {
        return organisms;
    }

    public int getRadiusOfMovement() {
        return radiusOfMovement;
    }

    public void setRadiusOfMovement(int radiusOfMovement) {
        this.radiusOfMovement = radiusOfMovement;
    }

    public void setCycleLength(long cycleLength) {
        this.cycleLength = cycleLength;
    }

    /** Get nearest organism to the one you pass into the function **/
    public ConcreteOrganism getNearestOrganism(ConcreteOrganism yourself) {
        try {
            double distance = 999.0;
            ConcreteOrganism solncache = yourself; //If only one organism, this may return self... If we don't want this functionality, change to null?

            for (ConcreteOrganism org : organisms) {
                double mydist = Math.abs(yourself.getPosition().getX() - org.getPosition().getX());
                mydist += Math.abs(yourself.getPosition().getY() - org.getPosition().getY());
                mydist += Math.abs(yourself.getPosition().getZ() - org.getPosition().getZ());
                if (mydist > 0 && mydist < distance) {
                    distance = mydist;
                    solncache = org;
                }
            }

            return solncache;
        } catch (ConcurrentModificationException ex) {
            return yourself;
        }
    }

    /** Get a list of organisms, sorted by how close to you they are **/
    public ArrayList<ConcreteOrganism> getNearestOrganisms(ConcreteOrganism yourself) {
        class dblDouble implements Comparable {
            //It's a double double! Perfect for sorting with Java's built in fast sort!
            public int index = 0; //These can be public, because they're inside of a function making them unaccessable anyway!
            public double value = 0;

            public int compareTo(Object o) {
                if (o instanceof dblDouble) {
                    double theirval = ((dblDouble) o).value;
                    if (theirval > value) {
                        return -1;
                    } else if (theirval == value) {
                        return 0;
                    } else {
                        return 1;
                    }
                }

                return 999; //Definately not equal, since they're completely different types.
            }
        }
        dblDouble[] distances = new dblDouble[organisms.size()];

        for (int i = 0; i < organisms.size(); i++) {
            ConcreteOrganism org = organisms.get(i);
            double mydist = Math.abs(yourself.getPosition().getX() - org.getPosition().getX());
            mydist += Math.abs(yourself.getPosition().getY() - org.getPosition().getY());
            mydist += Math.abs(yourself.getPosition().getZ() - org.getPosition().getZ());
            distances[i] = new dblDouble();
            distances[i].value = mydist;
            distances[i].index = i;
        }
        //Great, now that's settled. We know all of your distances. (and your secrets too)

        //Now we need to quickly sort everything, to see which is the third most far away.
        java.util.Arrays.sort(distances);

        //Great, now we just need to build the output list
        ArrayList<ConcreteOrganism> output = new ArrayList();
        for (int i = 1; i < distances.length; i++) {
            output.add(organisms.get(distances[i].index));
        }

        return output;
    }

    /**
     * Finds the average position of the group of this color
     * @param searchColor
     * @return
    public Position centerOfMass(Color searchColor) {
    double x = 0;
    double y = 0;
    double z = 0;
    long familySize = 0;
    for (int i = 0; i < organisms.size(); i++) {
    if (organisms.get(i).isInColorFamily(searchColor)) {
    familySize++;
    x += organisms.get(i).getPosition().getX();
    /**
     * Finds the average position of the group of this color
     * @param searchColor
     * @return
     */
    public Position centerOfMass(Color searchColor) {
        double x = 0;
        double y = 0;
        double z = 0;
        long familySize = 0;

        for (int i = 0; i < organisms.size(); i++) {
            if (organisms.get(i).isInColorFamily(searchColor)) {
                familySize++;
                x += organisms.get(i).getPosition().getX();
                y += organisms.get(i).getPosition().getY();
                z += organisms.get(i).getPosition().getZ();
            }
        }

        x = x / (double) familySize;
        y = y / (double) familySize;
        z = z / (double) familySize;

        return new Position(x, y, z);
    }

    /**
     * Find the closest organism that is edible by yourself
     * @param yourself
     * @return
     */
    public ConcreteOrganism getClosestEdible(ConcreteOrganism yourself) {
        ArrayList<ConcreteOrganism> sortedList = this.getNearestOrganisms(yourself);
        ConcreteOrganism closest = null;

        for (int i = 0; i < sortedList.size() && closest == null; i++) {
            if (yourself.canEat(sortedList.get(i))) {
                closest = sortedList.get(i);
            }
        }

        System.out.println(yourself+" thinks this is edible: "+closest);
        
        return closest;
    }

    public void setCycles(long cycles) {
        this.cycles = cycles;
        numCycles = cycles;
    }

    public void setHealthmod(int healthmod) {
        this.healthmod = healthmod;
    }
    
    /** Load the stock traits from the traits folder **/
    private void loadStockTraits(){
        File traitdir = new File("traits"+File.separator);
        if(!traitdir.exists()){
            traitdir.mkdir();
        }
        File[] loadthem = traitdir.listFiles();
        
        for(int i=0; i<loadthem.length; i++){
            try{
                if(loadthem[i].getAbsolutePath().toLowerCase().endsWith(".xml")){
                    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
                    DocumentBuilder loader = factory.newDocumentBuilder();
                    Document loadDocument = loader.parse(loadthem[i]);

                    NodeList docnodes = loadDocument.getChildNodes();
                    if(docnodes.item(0).getNodeName().equals("protovitae_gene")){
                        ConcreteGene geneX = new ConcreteGene(null);
                        geneX.load((Element)docnodes.item(0));
                        masterlist.addStockTrait(geneX);
                    }
                }
            }
            catch(Exception e){
                e.printStackTrace();
            }
        }
    }

    /** BEGIN Statistics getters: **/
    /** Get number of organisms added **/
    public int getNumAdded() {
        return numAdded;
    }

    public int getNumDeleted() {
        return numDeleted;
    }

    public int getNumDeaths() {
        return numDeaths;
    }

    public long getNumCycles() {
        return numCycles;
    }

    public TraitTree getMasterlist() {
        return masterlist;
    }
    /** END OF Statistics getters **/
}
